接下來這一篇將會來介紹 JWT 的驗證,畢竟前面一直都沒提到,可是實戰上來講卻是非常重要的一環哩。
為什麼要身份認證呢?前面的許多章節中,其實我們都沒有做身份認證的機制,舉凡 API、Discord、Chrome Extension 等等,都沒有做身份認證,那麼為什麼要做身份認證呢?
我們所開發的東西有些功能是僅限於特定的人才能使用,例如:管理員、VIP 等等,這時候我們就需要一個機制來做身份認證,那麼這時候就會提到 JWT(JSON Web Token)了。
而現今主流開發比較常見於前後端分離的架構,不再是傳統的 SSR(Server Side Rendering)的架構,前端大多都是透過後端所提供的 API 來取得資料,而這個取資料的過程就必須要被認證。
Note
早期開發時,使用者登入後,後端會將使用者的 UID 儲存在伺服器的記憶體中(Session),但因為現今主流開發事前後端分離的架構,因此這種方式已經不適用了,因此就有了 JWT。
剛才有提到 JWT 的全名是 JSON Web Token,是一個廣泛被使用來驗證與授權的標準,尤其是在網頁開發上很常被使用,而且也是一個開放的標準(RFC 7519)。
基本上 JWT 的結構分為三個部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
很難看出其差異吧?如果我依據上面的結構來拆解的話,就會變成這樣:
HEADER.PAYLOAD.VERIFY SIGNATURE
搭配上方的 JWT 範例,就會變成這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. # HEADER(標頭)
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # PAYLOAD(負載)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # VERIFY SIGNATURE(驗證簽名)
那麼 JWT 是如何實作出來的呢?剛才有提到 JWT 的結構分為三個部分,其實比較核心的部分是 PAYLOAD(負載)與 VERIFY SIGNATURE(驗證簽名),但我們還是針對 JWT 三個部分來做介紹,而這邊示範的範例會是前面所講的 JWT Token 範例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. # HEADER(標頭)
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # PAYLOAD(負載)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # VERIFY SIGNATURE(驗證簽名)
HEADER 主要組合會是兩個部分:
而這兩個部分會被編碼成 Base64 字串,例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. # HEADER(標頭)
PAYLOAD(負載)的部分,其實就是一個 JSON 物件,例如:
{
"username": "Ray", // 使用者名稱
"email": "example.com", // 使用者信箱
"iat": 1516239022 // 簽發(Issued At)時間
}
而這個 JSON 物件會被編碼成 Base64 字串,例如:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # PAYLOAD(負載)
感覺 PAYLOAD 滿單純的吧?但實際上它是由三個部分組成的,分別是:
而這三個部分都是選填的,但實戰上來講,通常都會使用 Public Claim(公開聲明)來存放一些資料,例如:使用者的 ID、名字等等。
比較常見的設定會是 Registered Claim 中的 iat(Issued At)與 exp(Expiration Time),而這兩個屬性可以用來驗證 Token 是否過期等。
當然,Registered Claim 中還有很多屬性,例如:
因此以上面前面的 JSON 物件來說,就會變成這樣:
{
"username": "Ray", // 使用者名稱 => Public Claim(公開聲明)
"email": "example.com", // 使用者信箱 => Public Claim(公開聲明)
"iat": 1516239022 // 簽發(Issued At)時間 => Registered Claim(註冊聲明)
}
Note
要注意一下 Claim(聲明)的單字只有三個字母,因為 JWT 旨在簡潔與輕量化,因此 Claim(聲明)的單字只有三個字母。
VERIFY SIGNATURE(驗證簽名)的部分,其實就是將 PAYLOAD(負載)與 HEADER(標頭)進行編碼,然後再進行加密,例如:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
那麼問題來了,JWT 是如何知道要用什麼演算法來加密呢?其實就是透過 HEADER(標頭)來取得,例如:
{
"alg": "HS256",
"typ": "JWT"
}
如果今天是用 PS256 演算法來加密的話,就會變成這樣:
{
"alg": "PS256",
"typ": "JWT"
}
接著就會將 PAYLOAD(負載)與 HEADER(標頭)進行編碼,然後再進行加密,例如:
PS256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
而這個加密後的字串也會被編碼成 Base64 字串。
那 secret 是什麼呢?其實就是一個私鑰,我們在針對 JWT TOken 生成時,會需要使用一組私鑰,而這組私鑰會被用來加密,而當我們要驗證 JWT Token 時,就會需要使用這組私鑰來解密。
那麼另一個問題來了,我們生成的 JWT Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
你可以把它丟到 jwt.io 這個網站上,然後你就會發現它會自動幫你解析,並且告訴你這個 Token 是用什麼演算法加密的,以及 Token 的類型,如下圖:
好的,問題來了
「為什麼我們沒有提供私鑰(secret),但是它卻可以解析出來呢?」
因為 JWT Token 只是透過 Base64 編碼,而並沒有進行加密,因此你是可以自己寫一個 JWT Token 解析器,這邊我也簡單示範寫一個 JWT Token 解析器範例:
// Base64 解碼
const Base64decoded = (str) => {
str = str.replace(/-/g, '+').replace(/_/g, '/');
// 如果字串長度不是 4 的倍數,補上 "="
while (str.length % 4 !== 0) {
str += '=';
}
return window.atob(str);
}
// 解析 JWT Token
const jwtTokenEncoded = (str) => {
if(!str) return console.log('請輸入 JWT Token');
const [header, payload, signature] = str.split('.');
const headerDecoded = JSON.parse(Base64decoded(header));
const payloadDecoded = JSON.parse(Base64decoded(payload));
return {
header: headerDecoded,
payload: payloadDecoded,
}
}
jwtTokenEncoded('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c')
接著你貼到瀏覽器上就可以看到解析結果了,如下圖:
因此實際上來講 JWT Token 是用 Base64 編碼的,而且有一定的安全性問題,所以千萬不要把敏感資訊放在 JWT Token 中,例如:使用者的密碼、使用者的信用卡資訊等等。
JWT Token 核心驗證的方式主要在於剛剛提到的 secret(私鑰),因此就算我們解析出來了,但是沒有私鑰的話,也是無法驗證成功。
那麼透過這一篇,我相信你應該對於 JWT Token 有一定的了解了,接下來我們就來看看如何實作 JWT Token 吧!
JWT Token 在 Node.js 上使用非常的簡單,只需要安裝 jsonwebtoken
套件
npm install jsonwebtoken
用法也很簡單,只需要安裝 jsonwebtoken
套件並這樣寫就可以了:
const jwt = require('jsonwebtoken');
const token = jwt.sign({
username,
email: 'example.com',
}, 'secret', { expiresIn: 60 });
console.log(token); // 這邊就會印出 JWT Token
jwt.sign
會接受三個參數:
而 jwt.sign
會回傳一個 JWT Token,這個 JWT Token 就是我們要發放給使用者的 Token。
往後前端需要請求某些需要權限或驗證的 API 時,就可以將這個 JWT Token 放在 Header 中,例如:
axios.get('https://example.com/api', {
headers: {
Authorization: `Bearer ${token}`,
},
});
接著後端只需要使用 jwt.verify
就可以驗證 JWT Token 是否正確,例如:
const jwt = require('jsonwebtoken');
const token = jwt.sign({
username,
email: 'example.com',
}, 'secret', { expiresIn: 60 });
const decoded = jwt.verify(token, 'secret');
console.log(decoded); // 這邊就會印出解析後的 JWT Token
是不是超簡單的呢?這邊我就不額外提供範例了,你可以再試著自己嘗試看看。
Note
Bearer
是一種 HTTP 認證方式,你可以參考 Authentication 這篇文章。
那麼這一篇也差不多了,我們下一篇見哩。